Reactivity: 実装方法を理解しよう
今回の目的: ステートが変更された時に updateComponent を実行させる
Vuejs ReactivitySystem 登場概念 一覧
target: リアクティビティにしたいオブジェクト
Proxy:
ReactiveEffect:
Dep: target の key に対して実行したい作用
track: targetMap に登録する関数
trigger: targetMap から作用を取り出して実行する関数
targetMap: target の key と Dep のマッピング
activeEffect: trackで targetMap で登録する関数
実装の流れ
1. まずは targetMap の構造から理解する
code: typescript
type Target = any // 任意のtarget
type TargetKey = any // targetが持つ任意のkey
const targetMap = new WeakMap<Target, KeyToDepMap>() // このモジュール内のグローバル変数として定義
type KeyToDepMap = Map<TargetKey, Dep> // targetのkeyと作用のマップ
type Dep = Set<ReactiveEffect> // depはReactiveEffectというものを複数持っている
class ReactiveEffect {
constructor(
// ここに実際に作用させたい関数を持たせます。 (今回でいうと、updateComponent)
public fn: () => T,
) {}
}
リアクティビティの基本的な構造は この targetMap が担っている
この targetMap をどう実装していくか
実際に作用を実行するにはどうするか
を次に考える
NOTE: なぜWeakMapを使っている? 普通のMapだと不都合があるのか?
-> GCのreferecnceCount として countされない
2. reactivity 関数の中身を理解する
code: typescript
// TargetMap に作用を登録する関数
export function track(target: object, key: unknown) {
// ..
}
// TargetMap から作用を取り出して実行する関数
export function trigger(target: object, key?: unknown) {
// ..
}
// get と set のハンドラに track と trigger を持つProxyを生成する関数 -> それが reactive!
function reactive<T>(target: T) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key) // targetのkeyに値を取得する際に、作用を登録する
},
set(target, key, value, receiver) {
targetkey = value // targetのkeyの値を更新する際に、登録してある作用を実行する trigger(target, key)
return true
},
})
}
https://scrapbox.io/files/65ffd42c9921330024541837.png
3. reactive が形成されるまで
track ではどの関数を targetMap に登録するのか?
-> activeEffect を利用する
code: typescript
// モジュール内のグローバル変数として定義
let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
constructor(
// ここに実際に作用させたい関数を持たせます。 (今回でいうと、updateComponent)
public fn: () => T,
) {}
run() {
activeEffect = this // runメソッドが走るたびに、activeEffect を更新する
return this.fn()
}
}
どんな原理?
下記のようなコンポーネントがあった場合
code: main.ts
{
setup() {
const state = reactive({ count: 0 });
const increment = () => state.count++;
return function render() {
return h("div", { id: "my-app" }, [
h("p", {}, [count: ${state.count}]),
h(
"button",
{
onClick: increment,
},
),
]);
};
},
}
これは内部的には下記のようにリファクティブを形成する
code: apiCreateApp.ts
const app: App = {
mount(rootContainer: HostElement) {
const componentRender = rootComponent.setup!()
const updateComponent = () => {
const vnode = componentRender()
render(vnode, rootContainer)
}
const effect = new ReactiveEffect(updateComponent)
effect.run()
},
}
順を追って説明
1. setup関数が実行される。この時点で reactive proxy が生成される
code: main.ts
const state = reactive({ count: 0 }) // proxyの生成
2. updateComponent を渡して ReactiveEffect (Observer 側)を生成する
この updateComponent で使っている componentRender は setup の戻り値の関数です。そしてこの関数は proxy によって作られたオブジェクトを参照しています
なので、この関数が走った時、state.count のgettter 関数が実行され、track が実行されることになる
code: main.ts
function render() {
return h('div', { id: 'my-app' }, [
h('p', {}, [count: ${state.count}]), // proxy によって作られたオブジェクトを参照している
h(
'button',
{
onClick: increment,
},
),
])
}
3. この状況下で effect を実行 (effect.run()) してみる
まず activeEffect に updateComponent が設定される
この状態で track が走るので targetMap に state.count と updateComponent のマップが登録される
-> これが reactive の形成!!
ここで、increment が実行された時のことを考えてみよう
increment では state.count を書き換えているので setter が実行され、trigger が実行される。
trigger は state と count を元に targetMap から effect (今回の例だと updateComponent )をみつけ、実行する。
-> 画面の更新が行われる!
以上の仕組みによってリアクティブを実現することができる
https://scrapbox.io/files/65ffda9ac3640c0024ee8ce3.png